package amazing.core.main;

import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.function.Consumer;

import amazing.core.server.*;
import amazing.core.logging.*;
import amazing.core.module.Modular;
import amazing.core.module.Module;
import amazing.core.module.ModuleAlreadyLoadedException;
import amazing.core.module.ModuleClassLoader;
import amazing.core.module.ModuleFailedToLoadException;
import amazing.core.module.ModuleFailedToUnloadException;
import amazing.core.module.ModuleNotFoundException;
import amazing.core.module.RequiredModule;
import amazing.core.module.RequiredModules;

public final class AmazingServer {
	private static ServerCore server;
	protected static Properties properties;
	//use this classloader to load any modules
	private static List<URLClassLoader> loaders = new ArrayList<>();
	private static ModuleManager mm;
	public static void main(String[] args) throws IOException, InterruptedException {
		properties = parseArguments(args);
		
		server = new ServerCore(properties);
		mm = ModuleManager.getInstance();
		server.start();
	}
	public static ServerCore getServer()
	{
		return server;
	}
	private static Properties parseArguments(String...args)
	{
		Properties props = new Properties();
		try (FileInputStream fis = new FileInputStream("server.properties"))
		{
			props.load(fis);
		} catch (IOException e) {
			props.setProperty("port", "80");			
		}
		return props;
	}
	public static final class ModuleManager
	{
		private List<Module> loadedModules = new ArrayList<>();
		private static ModuleManager instance = null;
		public static ModuleManager getInstance()
		{
			if (instance == null)
			{
				try
				{
					instance = new ModuleManager();
				}
				catch (Exception e)
				{
					Logger.log(LogLevel.Fatal, "Could not load module manager: " + e.getMessage());
					e.printStackTrace();
					System.exit(1);
				}
			}
			
			return instance;
		}
		private void close()
		{
			//sec
			for (Iterator<Module> iter = loadedModules.iterator(); iter.hasNext();)
			{
				try {
					Module m = iter.next();
					m.close();
					iter.remove();//remove it from the list as well as close it
				} catch (Exception e) {
					
				}
			}
			
			instance = null;
		}
		
		private ModuleManager() throws ClassNotFoundException, InstantiationException, IllegalAccessException, ModuleNotFoundException, ModuleFailedToLoadException, ModuleAlreadyLoadedException, IOException {	
			String dirProp = properties.getProperty("modulesDirectories");
			if (dirProp == null)
				dirProp = "modules;amazing/test/";
			for (String dir : dirProp.split(";"))
			{
				loadAllModulesInDirectory(dir);
			}
		}
		private URLClassLoader getClassLoader(String directory) throws MalformedURLException
		{
			URL dir = new File(directory).toURI().toURL();
			URLClassLoader clsLoader = null;
			for (URLClassLoader ucl : loaders)
				for (URL url : ucl.getURLs())
					if (url.getPath().equalsIgnoreCase(dir.getPath()))
						clsLoader = ucl;
			if (clsLoader == null)
			{
				clsLoader = new URLClassLoader(new URL[]{ dir });
				loaders.add(clsLoader);
			}
			return clsLoader;
		}
		public void loadAllModulesInDirectory(String directory) throws ClassNotFoundException, InstantiationException, IllegalAccessException, ModuleNotFoundException, ModuleFailedToLoadException, ModuleAlreadyLoadedException, IOException
		{
			loadModuleByModuleName(directory, null);
		}
		public void loadModuleByModuleName(String directory, String name) throws ClassNotFoundException, ModuleNotFoundException, InstantiationException, IllegalAccessException, ModuleFailedToLoadException, ModuleAlreadyLoadedException, IOException
		{
			Logger.log("Looking for module by module name [" + directory + ":" + name + "]");
			URLClassLoader clsLoader = getClassLoader(directory);
			File f = new File(new File(directory).getPath());
			FilenameFilter ff = new FilenameFilter()
			{

				@Override
				public boolean accept(File file, String name) {
					// TODO Auto-generated method stub
					return name.endsWith(".class") || name.endsWith(".jar");
				}
				
			};
			for (File file : f.listFiles(ff))
			{
				String clsname = file.getCanonicalPath().replace(new File("").getCanonicalPath(), "").substring(1).replaceFirst("[.][^.]+$", "");
				if (clsname.equals(""))
					clsname = file.getCanonicalPath().replace(new File("").getCanonicalPath(), "").substring(1);
				Class<?> clazz = clsLoader.loadClass(clsname.replaceAll("[\\\\]", "."));
				if (!clazz.isAnnotationPresent(Modular.class))
					continue;
				Modular mod = clazz.getAnnotation(Modular.class);
				if (name == null || mod.name().equalsIgnoreCase(name))
				{
					//we found the class
					loadModule(directory, clazz);
				}
			}
			if (name != null)
				throw new ModuleNotFoundException("There was no module found with the name " + name);
		}
		
		public void loadModuleByClassName(String directory, String classname) throws ModuleFailedToLoadException, MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException, ModuleAlreadyLoadedException
		{
			URLClassLoader clsLoader = getClassLoader(directory);

			Class<?> cls = clsLoader.loadClass(classname);
			loadModule(directory, cls);
			
		}
		private void loadModule(String dir, Class<?> cls) throws ModuleFailedToLoadException, InstantiationException, IllegalAccessException, ModuleAlreadyLoadedException
		{
			Modular mmod = cls.getAnnotation(Modular.class);
			Logger.log("Loading module " + mmod.name() + "...");
			for (Module m : loadedModules)
			{
				if (m.getClass().equals(cls))
				{
					Logger.log("Module already loaded: " + mmod.name());
					return;
					//throw new ModuleAlreadyLoadedException("Module " + cls.getName() + " is already loaded.");
				}
			}
			
			if (cls.isAnnotationPresent(RequiredModules.class))
			{
				Logger.log("Searching for required modules for " + mmod.name());
				RequiredModule[] mods = cls.getAnnotation(RequiredModules.class).value();
				for (RequiredModule mod : mods)
				{
					try
					{
						Logger.log("Attempting to load required module " + mod.moduleName() + " for module " + mmod.name());
						loadModuleByModuleName(mod.importFrom().equals("") ? dir : mod.importFrom(), mod.moduleName());
					} catch(ModuleAlreadyLoadedException e)
					{
						Logger.log(LogLevel.Normal, "Tried to load a required module that was already loaded. ["+mod.moduleName() + "]");
					} catch (Exception e)
					{
						throw new ModuleFailedToLoadException(e);
					}
				}
			}
			Logger.log("Starting up module [" + mmod.name() + "] ...");
			Module m = Module.class.cast(cls.newInstance());
			
			m.open();
			loadedModules.add(m);
			this.notifyAdd(m);		
			//also have to figure out
		}
		public boolean isLoaded(Class<? extends Module> m) 
		{
			for (Module mod : loadedModules)
				if (mod.getClass().getAnnotation(Modular.class).name().equals(m.getAnnotation(Modular.class).name()))
					return true;
			return false;
					
		}

		public void unloadModule(Module m) throws ModuleFailedToUnloadException
		{
			Logger.log("Attempting to unload module " + (m.getClass().getAnnotation(Modular.class).name()));
			if (!isLoaded(m.getClass()))
			{
				Logger.log((m.getClass().getAnnotation(Modular.class).name()) + " is not loaded.");
				return;
			}
			
			StackTraceElement[] stes = new Throwable().getStackTrace();
			StackTraceElement ste = stes[stes.length - 2]; //unloadModule = -1, caller = -2
			boolean okay = false;
			final Modular mnote = m.getClass().getAnnotation(Modular.class);

			try {
				
				Class<?> cls = Class.forName(ste.getClassName());
				if (!cls.equals(m.getClass()) && !cls.equals(getClass()))
				{
					//it is not the same type as the class being unloaded, and it's not this type
					//check if the module was loaded from a RequiredModule annotation
					for (Module mod : loadedModules)
					{
						if (mod.getClass().isAnnotationPresent(RequiredModules.class))
						{
							RequiredModule[] rmods = mod.getClass().getAnnotation(RequiredModules.class).value();
							for (RequiredModule rm : rmods)
							{
								if (mnote.name().equalsIgnoreCase(rm.moduleName()))
								{
									//unloaded module was required by mod, check if any other classes are requiring it
									boolean any = false;
									List<RequiredModule> l = new ArrayList<>();
									for (RequiredModule mr : rmods)
										l.add(mr);

									for (RequiredModule r : l)
										if (!r.moduleName().equalsIgnoreCase(mnote.name()) && r.moduleName().equalsIgnoreCase(rm.moduleName()))
											any = true; 
									if (any)
									{
										//more modules require it, do not allow any module but itself or ModuleManager to remove it unless
										//there is only 1 module that requires it, if so make the assumption that the module being unloaded
										//was obtained from being required and not already loaded
										String msg = (cls.isAnnotationPresent(Modular.class) ? 
												"The module " + cls.getAnnotation(Modular.class).name() : 
												"The class " + cls.getName()) + 
												" tried to unload module " + mnote.name() + " but that class was not the only class requiring it.";
										Logger.log(LogLevel.High, msg);
										throw new ModuleFailedToUnloadException(msg);
									} else
									{
										String name = mnote.name();
										String ver = mnote.version();
										//only 1 module required this one, assume it was imported and allow it to be unloaded
										try {
											m.close();
											loadedModules.remove(m);
											Logger.log("Unloaded module " + name + "version " + ver);
											notifyRemoval(m);
											return;
										} catch (Exception e) {
										}
									}
								}
							}
						}
					}
					String msg = (cls.isAnnotationPresent(Modular.class) ? 
							"The module " + cls.getAnnotation(Modular.class).name() : 
							"The class " + cls.getName()) + 
							" tried to unload module " + mnote.name() + " but that class does not have access for removal.";
					Logger.log(LogLevel.High, msg);
					throw new ModuleFailedToUnloadException(msg);
				}
				

				
			} catch (ClassNotFoundException e1) { }
			if (!okay)
				return;
			String name = mnote.name();
			String ver = mnote.version();
			Logger.log("Unloading module " + name);
			
			try {
				m.close();
				loadedModules.remove(m);
				Logger.log("Unloaded module " + name + "version " + ver);
				notifyRemoval(m);
			} catch (Exception e) {
				Logger.log(LogLevel.High,"Error unloading module: " +  e.getMessage());
				throw new ModuleFailedToUnloadException(e.getMessage());
			}
		}
		
		private void notifyRemoval(Module m)
		{
			Logger.log("Notifying other modules loaded of removed module " + m.getClass().getAnnotation(Modular.class).name());

			for (Module r : loadedModules)
				if (!r.getClass().getAnnotation(Modular.class).name().equalsIgnoreCase(m.getClass().getAnnotation(Modular.class).name()))
					r.notifyModuleRemoved(m);			
		}
		
		private void notifyAdd(Module m)
		{
			Logger.log("Notifying other modules loaded of added module " + m.getClass().getAnnotation(Modular.class).name());

			for (Module r : loadedModules)
				if (!r.getClass().getAnnotation(Modular.class).name().equalsIgnoreCase(m.getClass().getAnnotation(Modular.class).name()))
					r.notifyModuleAdded(m);
		}
	}
}
